package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 聊天室服务端
 */
public class Server {
    /*
        java.net.ServerSocket
        运行在服务端，相当于客服的"总机"。用于接收不同客户端的连接
     */
    private ServerSocket serverSocket;
    //线程池
    private ExecutorService threadPool;

    //该集合用于存放所有客户端的输出流
//    private List<PrintWriter> allOut = new ArrayList<>();

    //使用并发安全的集合
//    private List<PrintWriter> allOut = Collections.synchronizedList(new ArrayList<>());

    /*
        创建一个Map来保存所有客户端的输出流以及该客户端的昵称

        java.util.HashMap,LinkedHashMap都不是并发安全的


        java.util.concurrent.ConcurrentHashMap是并发安全的HashMap

        java.util.concurrent:java并发包，里面保存了关于并发的相关API
        该包在行业中经常叫做:JUC
     */
    private Map<String,PrintWriter> allOut = new ConcurrentHashMap<>();

    public Server(){
        try {
            /*
                实例化ServerSocket时需要指定端口，客户端的Socket就是通过这个端口找到
                ServerSocket并与之连接的
                如果该端口被其他应用程序占用会抛出异常:
                java.net.BindException:address already in use
                此时需要更换端口再次尝试
             */
            System.out.println("正在启动服务端...");
            serverSocket = new ServerSocket(8088);
            threadPool= Executors.newFixedThreadPool(50);

            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public void start(){
        try {
            /*
                ServerSocket有一个重要的方法:
                Socket accept()
                该方法用于接受客户端的连接，这个方法是一个阻塞方法，调用该方法后不会立即返回
                此时会"卡主"，直到一个客户端连接方法才会返回，返回的Socket就是与该客户端建立
                连接的。
             */
            while(true) {
                System.out.println("等待客户端连接...");
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端连接了!");
                //启动线程来与该客户端交互
                ClientHandler handler = new ClientHandler(socket);
                threadPool.execute(handler);
               // Thread t = new Thread(handler);
                //t.start();

            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    /**
     * 该线程任务负责与指定客户端交互
     */
    private class ClientHandler implements Runnable{
        private Socket socket;
        private String host;//记录该客户端的IP地址
        private String nickName;//记录该客户端的昵称

        public ClientHandler(Socket socket){
            this.socket = socket;
            //通过socket获取远端计算机的IP地址信息
            host = socket.getInetAddress().getHostAddress();
        }

        public void run(){
            PrintWriter pw = null;
            try {
                /*
                    Socket提供的方法:
                    InputStream getInputStream()
                    通过Socket获取一个字节输入流，用于读取来自远端计算机发送过来的字节数据
                 */
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);
                //读取客户端发送过来的第一行字符串作为昵称
                nickName = br.readLine();

                //通过socket获取输出流，用于将消息发送给客户端
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw,true);

//                synchronized (allOut) {
//                    //将该客户端的输出流存入共享集合
//                    allOut.add(pw);
//                }
                //将该客户端的输出流存入共享集合
//                allOut.add(pw);//并发安全的集合，多个线程调用add本身就是同步的

                //将该用户的昵称作为key，它的输出流作为value存入map
                allOut.put(nickName,pw);


                //告知所有客户端该用户上线了
                sendMessage(nickName+"("+host+")上线了!当前在线人数:"+allOut.size());


                String line;
                /*
                    readLine方法用于读取来自客户端发送过来的一行字符串。
                    该方法有以下几种现象:
                    1:正常返回一行字符串，正常读取
                    2:返回null，说明客户端进行了四次挥手与服务端断开链接，不会再发送数据
                    3:抛出异常，客户端没有进行四次挥手而异常断开
                 */
                while ((line = br.readLine()) != null) {//读取对方发送过来的一行字符串
                    //如果聊天消息以“@”开始则认为是私聊
                    if(line.startsWith("@")){
                        sendMessageToSomeOne(line);
                    }else {
                        //将消息广播给所有客户端
                        sendMessage(nickName + "(" + host + ")说:" + line);
                        //
                        saveMessage(nickName,null,line);
                    }
                }
            } catch (IOException e) {

            } finally {
                //处理客户端断开连接后的操作

//                synchronized (allOut) {
//                    //将该客户端的输出流从共享集合allOut中删除
//                    allOut.remove(pw);
//                }

                //将该客户端的输出流从共享集合allOut中删除
//                allOut.remove(pw);

                allOut.remove(nickName);

                //广播消息告知所有客户端该用户下线了
                sendMessage(nickName+"("+host+")下线了!当前在线人数:"+allOut.size());

                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        /**
         * 广播消息给所有客户端
         * @param message
         */
        public void sendMessage(String message){
            //先将消息输出到服务端控制台
            System.out.println(message);
//            synchronized (allOut) {
//                //将消息广播给所有客户端
//                for (PrintWriter o : allOut) {
//                    o.println(message);
//                }
//            }

            /*
                将消息广播给所有客户端
                并发安全的集合也不保证与迭代器操作互斥，因此要采取使用集合自身的方法
                forEach来遍历集合，保证与其他操作互斥达到并发安全的目的
             */
//            allOut.forEach(o->o.println(message));

            allOut.values().forEach(o->o.println(message));
        }

        /**
         * 将消息发送给指定用户(私聊功能)
         * @param message  私聊消息，格式应当是:@昵称:聊天内容
         */
        public void sendMessageToSomeOne(String message){
            /*
                message:
                @克晶:你好
             */
            //必要的格式验证
            if(message.matches("@.+:.+")){
                //@克晶:你好
                //1截取出来对方的昵称
                String toNickName = message.substring(1,message.indexOf(":"));

                //2判断对方是否存在
                if(allOut.containsKey(toNickName)){
                    //3截取出聊天信息并发送给对方
                    String toMessage = message.substring(message.indexOf(":")+1);
                    //根据对方的昵称获取到他的输出流，将消息单独发送给他
                    PrintWriter pw = allOut.get(toNickName);
                    //私聊发给对方的格式:XXX悄悄和你说:聊天消息
                    pw.println(nickName+"悄悄对你说:"+toMessage);
                    saveMessage(nickName,toNickName,toMessage);

                }else{//对方不存在                  用户[克晶]不存在!
                    allOut.get(nickName).println("用户["+toNickName+"]不存在!");
                }
            }else{
                //私聊消息格式不对，通知当前用户
                allOut.get(nickName).println("私聊格式不正确，正确格式:@昵称:聊天信息");
            }
        }


        public  void  saveMessage(String fromUser,String toUser,String content ){
            try (Connection connection=DBUtil.getConnection()){
                String sql="INSERT INTO chatinfo(from_user,to_user,content)" +
                        "VALUES (?,?,?)";
                PreparedStatement ps = connection.prepareStatement(sql);
                ps.setString(1,fromUser);
                ps.setString(2,toUser);
                ps.setString(3,content);
                ps.executeUpdate();


            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }


    }
}
